{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {
    "deletable": true,
    "editable": true
   },
   "source": [
    "# Pynamical: animated 3D phase diagrams of the logistic map\n",
    "\n",
    "Author: Geoff Boeing\n",
    "\n",
    "Journal article: Boeing, G. 2016. \"[Visual Analysis of Nonlinear Dynamical Systems: Chaos, Fractals, Self-Similarity and the Limits of Prediction](http://geoffboeing.com/publications/nonlinear-chaos-fractals-prediction/).\" *Systems*, 4 (4), 37. doi:10.3390/systems4040037.\n",
    "\n",
    "Blog post: http://geoffboeing.com/2015/04/animated-3d-plots-python/\n",
    "\n",
    "This notebook demonstrates how to make animated GIFs that pan and zoom around 3-D phase diagrams to visualize fractal data sets, strange attractors, and chaos."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {
    "collapsed": false,
    "deletable": true,
    "editable": true
   },
   "outputs": [],
   "source": [
    "import pynamical\n",
    "from pynamical import simulate, phase_diagram_3d\n",
    "import pandas as pd, numpy as np, matplotlib.pyplot as plt, random, glob, os, IPython.display as IPdisplay\n",
    "from PIL import Image\n",
    "%matplotlib inline"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "collapsed": true
   },
   "outputs": [],
   "source": [
    "title_font = pynamical.get_title_font()\n",
    "label_font = pynamical.get_label_font()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "collapsed": true,
    "deletable": true,
    "editable": true
   },
   "outputs": [],
   "source": [
    "save_folder = 'images/phase-animate'"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "deletable": true,
    "editable": true
   },
   "source": [
    "## Create a 3-D phase diagram as an animated gif that pans, rotates, and zooms. This demonstrates how the viewing perspective is composed of an elevation, a distance, and an azimuth."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {
    "collapsed": false,
    "deletable": true,
    "editable": true
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<img src=\"images/phase-animate/01-pan-rotate-zoom-demo.gif\"/>"
      ],
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# set a filename, run the logistic model, and create the plot\n",
    "gif_filename = '01-pan-rotate-zoom-demo'\n",
    "working_folder = '{}/{}'.format(save_folder, gif_filename)\n",
    "if not os.path.exists(working_folder):\n",
    "    os.makedirs(working_folder)\n",
    "    \n",
    "pops = simulate(num_gens=1000, rate_min=3.99, num_rates=1)\n",
    "fig, ax = phase_diagram_3d(pops, remove_ticks=False, show=False, save=False)\n",
    "\n",
    "# create 36 frames for the animated gif\n",
    "steps = 36\n",
    "\n",
    "# a viewing perspective is composed of an elevation, distance, and azimuth\n",
    "# define the range of values we'll cycle through for the distance of the viewing perspective\n",
    "min_dist = 7.\n",
    "max_dist = 10.\n",
    "dist_range = np.arange(min_dist, max_dist, (max_dist-min_dist)/steps)\n",
    "\n",
    "# define the range of values we'll cycle through for the elevation of the viewing perspective\n",
    "min_elev = 10.\n",
    "max_elev = 60.\n",
    "elev_range = np.arange(max_elev, min_elev, (min_elev-max_elev)/steps)\n",
    "\n",
    "# now create the individual frames that will be combined later into the animation\n",
    "for azimuth in range(0, 360, int(360/steps)):\n",
    "    \n",
    "    # pan down, rotate around, and zoom out\n",
    "    ax.azim = float(azimuth/3.)\n",
    "    ax.elev = elev_range[int(azimuth/(360./steps))]\n",
    "    ax.dist = dist_range[int(azimuth/(360./steps))]\n",
    "    \n",
    "    # set the figure title to the viewing perspective, and save each figure as a .png\n",
    "    fig.suptitle('elev={:.1f}, azim={:.1f}, dist={:.1f}'.format(ax.elev, ax.azim, ax.dist))\n",
    "    plt.savefig('{}/{}/img{:03d}.png'.format(save_folder, gif_filename, azimuth))\n",
    "    \n",
    "# don't display the static plot...\n",
    "plt.close()\n",
    "\n",
    "# load all the static images into a list then save as an animated gif\n",
    "gif_filepath = '{}/{}.gif'.format(save_folder, gif_filename)\n",
    "images = [Image.open(image) for image in glob.glob('{}/*.png'.format(working_folder))]\n",
    "gif = images[0]\n",
    "gif.info['duration'] = 75 #milliseconds per frame\n",
    "gif.info['loop'] = 0 #how many times to loop (0=infinite)\n",
    "gif.save(fp=gif_filepath, format='gif', save_all=True, append_images=images[1:])\n",
    "IPdisplay.Image(url=gif_filepath)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "deletable": true,
    "editable": true
   },
   "source": [
    "## Create a 3-D phase diagram as an animated gif starts by looking straight down at the x-y plane (this is what a 2-D plot would look like), then panning and rotating around to show the 3-D structure"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {
    "collapsed": false,
    "deletable": true,
    "editable": true
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<img src=\"images/phase-animate/02-pan-rotate-logistic-phase-diagram.gif\"/>"
      ],
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# set a filename, run the logistic model, and create the plot\n",
    "gif_filename = '02-pan-rotate-logistic-phase-diagram'\n",
    "working_folder = '{}/{}'.format(save_folder, gif_filename)\n",
    "if not os.path.exists(working_folder):\n",
    "    os.makedirs(working_folder)\n",
    "    \n",
    "pops = simulate(num_gens=1000, rate_min=3.99, num_rates=1)\n",
    "fig, ax = phase_diagram_3d(pops, color='#003399', xlabel='Population (t)', ylabel='Population (t + 1)', zlabel='', \n",
    "                           show=False, save=False)\n",
    "\n",
    "# look straight down at the x-y plane to start off\n",
    "ax.elev = 89.9\n",
    "ax.azim = 270.1\n",
    "ax.dist = 11.0\n",
    "\n",
    "# sweep the perspective down and rotate to reveal the 3-D structure of the strange attractor\n",
    "for n in range(0, 100):\n",
    "    if n > 19 and n < 23:\n",
    "        ax.set_xlabel('')\n",
    "        ax.set_ylabel('') #don't show axis labels while we move around, it looks weird\n",
    "        ax.elev = ax.elev-0.5 #start by panning down slowly\n",
    "    if n > 22 and n < 37:\n",
    "        ax.elev = ax.elev-1.0 #pan down faster\n",
    "    if n > 36 and n < 61:\n",
    "        ax.elev = ax.elev-1.5\n",
    "        ax.azim = ax.azim+1.1 #pan down faster and start to rotate\n",
    "    if n > 60 and n < 65:\n",
    "        ax.elev = ax.elev-1.0\n",
    "        ax.azim = ax.azim+1.1 #pan down slower and rotate same speed\n",
    "    if n > 64 and n < 74:\n",
    "        ax.elev = ax.elev-0.5\n",
    "        ax.azim = ax.azim+1.1 #pan down slowly and rotate same speed\n",
    "    if n > 73 and n < 77:\n",
    "        ax.elev = ax.elev-0.2\n",
    "        ax.azim = ax.azim+0.5 #end by panning/rotating slowly to stopping position   \n",
    "    if n > 76: #add axis labels at the end, when the plot isn't moving around\n",
    "        ax.set_xlabel('Population (t)')\n",
    "        ax.set_ylabel('Population (t + 1)')\n",
    "        ax.set_zlabel('Population (t + 2)')\n",
    "        \n",
    "    # add a figure title to each plot then save the figure to the disk\n",
    "    fig.suptitle('Logistic Map, r=3.99', fontsize=16, x=0.5, y=0.85)\n",
    "    plt.savefig('{}/{}/img{:03d}.png'.format(save_folder, gif_filename, n), bbox_inches='tight')\n",
    "\n",
    "# don't display the static plot\n",
    "plt.close()\n",
    "\n",
    "# load all the static images into a list then save as an animated gif\n",
    "gif_filepath = '{}/{}.gif'.format(save_folder, gif_filename)\n",
    "images = [Image.open(image) for image in glob.glob('{}/*.png'.format(working_folder))]\n",
    "gif = images[0]\n",
    "gif.info['duration'] = 10 #milliseconds per frame\n",
    "gif.info['loop'] = 0 #how many times to loop (0=infinite)\n",
    "gif.save(fp=gif_filepath, format='gif', save_all=True, append_images=images[1:])\n",
    "IPdisplay.Image(url=gif_filepath)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "deletable": true,
    "editable": true
   },
   "source": [
    "## Do the same thing again, but this time plot both the chaotic logistic model output and random noise"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {
    "collapsed": false,
    "deletable": true,
    "editable": true
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>chaos</th>\n",
       "      <th>random</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>995</th>\n",
       "      <td>0.247214</td>\n",
       "      <td>0.092325</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>996</th>\n",
       "      <td>0.742535</td>\n",
       "      <td>0.601917</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>997</th>\n",
       "      <td>0.762795</td>\n",
       "      <td>0.670351</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>998</th>\n",
       "      <td>0.721947</td>\n",
       "      <td>0.390911</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>999</th>\n",
       "      <td>0.800951</td>\n",
       "      <td>0.212530</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "        chaos    random\n",
       "995  0.247214  0.092325\n",
       "996  0.742535  0.601917\n",
       "997  0.762795  0.670351\n",
       "998  0.721947  0.390911\n",
       "999  0.800951  0.212530"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# run the logistic model and create random noise\n",
    "chaos_pops = simulate(num_gens=1000, rate_min=3.99, num_rates=1)\n",
    "random_pops = pd.DataFrame([random.random() for _ in range(0, 1000)], columns=['value'])\n",
    "pops = pd.concat([chaos_pops, random_pops], axis=1)\n",
    "pops.columns = ['chaos', 'random']\n",
    "pops.tail()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {
    "collapsed": false,
    "deletable": true,
    "editable": true
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<img src=\"images/phase-animate/03-pan-rotate-logistic-random.gif\"/>"
      ],
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# set a filename and then create the plot\n",
    "gif_filename = '03-pan-rotate-logistic-random'\n",
    "working_folder = '{}/{}'.format(save_folder, gif_filename)\n",
    "if not os.path.exists(working_folder):\n",
    "    os.makedirs(working_folder)\n",
    "    \n",
    "fig, ax = phase_diagram_3d(pops, color=['#003399','#cc0000'], xlabel='Population (t)', ylabel='Population (t + 1)', \n",
    "                           zlabel='', legend=True, legend_bbox_to_anchor=(0.94, 0.96), show=False, save=False)\n",
    "\n",
    "# configure the initial viewing perspective to look straight down at the x-y plane\n",
    "ax.elev = 89.9\n",
    "ax.azim = 270.1\n",
    "ax.dist = 11.0\n",
    "\n",
    "# sweep the perspective down and rotate to reveal the 3-D structure of the strange attractor\n",
    "for n in range(0, 100):\n",
    "    if n >= 20 and n <= 22:\n",
    "        ax.set_xlabel('')\n",
    "        ax.set_ylabel('') #don't show axis labels while we move around, it looks weird\n",
    "        ax.elev = ax.elev-0.5 #start by panning down slowly\n",
    "    if n >= 23 and n <= 36:\n",
    "        ax.elev = ax.elev-1.0 #pan down faster\n",
    "    if n >= 37 and n <= 60:\n",
    "        ax.elev = ax.elev-1.5\n",
    "        ax.azim = ax.azim+1.1 #pan down faster and start to rotate\n",
    "    if n >= 61 and n <= 64:\n",
    "        ax.elev = ax.elev-1.0\n",
    "        ax.azim = ax.azim+1.1 #pan down slower and rotate same speed\n",
    "    if n >= 65 and n <= 73:\n",
    "        ax.elev = ax.elev-0.5\n",
    "        ax.azim = ax.azim+1.1 #pan down slowly and rotate same speed\n",
    "    if n >= 74 and n <= 76:\n",
    "        ax.elev = ax.elev-0.2\n",
    "        ax.azim = ax.azim+0.5 #end by panning/rotating slowly to stopping position   \n",
    "    if n == 77: #add axis labels at the end, when the plot isn't moving around\n",
    "        ax.set_xlabel('Population (t)')\n",
    "        ax.set_ylabel('Population (t + 1)')\n",
    "        ax.set_zlabel('Population (t + 2)')\n",
    "        \n",
    "    # add a figure title to each plot then save the figure to the disk\n",
    "    fig.suptitle(u'3-D phase diagram, chaos vs random', fontsize=16, x=0.5, y=0.85)\n",
    "    plt.savefig('{}/{}/img{:03d}.png'.format(save_folder, gif_filename, n), bbox_inches='tight')\n",
    "\n",
    "# don't display the static plot\n",
    "plt.close()\n",
    "\n",
    "# load all the static images into a list then save as an animated gif\n",
    "gif_filepath = '{}/{}.gif'.format(save_folder, gif_filename)\n",
    "images = [Image.open(image) for image in glob.glob('{}/*.png'.format(working_folder))]\n",
    "gif = images[0]\n",
    "gif.info['duration'] = 10 #milliseconds per frame\n",
    "gif.info['loop'] = 0 #how many times to loop (0=infinite)\n",
    "gif.save(fp=gif_filepath, format='gif', save_all=True, append_images=images[1:])\n",
    "IPdisplay.Image(url=gif_filepath)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "deletable": true,
    "editable": true
   },
   "source": [
    "## Create a 3-D phase diagram to show the logistic map's strange attractors across the chaotic regime (from r=3.6 to r=4.0), twisting and curling around their state space in three dimensions. Animated it by panning and rotating to reveal the structure and its odd folds."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {
    "collapsed": false,
    "deletable": true,
    "editable": true
   },
   "outputs": [],
   "source": [
    "# run the model for 2,000 generations for 50 growth rate parameters between 3.6 and 4.0\n",
    "pops = simulate(num_gens=2000, rate_min=3.6, rate_max=4.0, num_rates=50)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {
    "collapsed": false,
    "deletable": true,
    "editable": true
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<img src=\"images/phase-animate/04-pan-rotate-chaotic-regime.gif\"/>"
      ],
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "execution_count": 9,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# set a filename and create the plot\n",
    "gif_filename = '04-pan-rotate-chaotic-regime'\n",
    "working_folder = '{}/{}'.format(save_folder, gif_filename)\n",
    "if not os.path.exists(working_folder):\n",
    "    os.makedirs(working_folder)\n",
    "    \n",
    "fig, ax = phase_diagram_3d(pops, color='viridis', color_reverse=False,\n",
    "                           xlabel='Population (t)', ylabel='Population (t + 1)', zlabel='', \n",
    "                           show=False, save=False)\n",
    "\n",
    "# configure the initial viewing perspective to look straight down at the x-y plane\n",
    "ax.elev = 89.9\n",
    "ax.azim = 270.1\n",
    "ax.dist = 11.0\n",
    "\n",
    "# sweep the perspective down and rotate to reveal the 3-D structure of the strange attractor\n",
    "for n in range(0, 100):\n",
    "    if n > 19 and n < 23:\n",
    "        ax.set_xlabel('')\n",
    "        ax.set_ylabel('') #don't show axis labels while we move around, it looks weird\n",
    "        ax.elev = ax.elev-0.5 #start by panning down slowly\n",
    "    if n > 22 and n < 37:\n",
    "        ax.elev = ax.elev-1.0 #pan down faster\n",
    "    if n > 36 and n < 61:\n",
    "        ax.elev = ax.elev-1.5\n",
    "        ax.azim = ax.azim+1.1 #pan down faster and start to rotate\n",
    "    if n > 60 and n < 65:\n",
    "        ax.elev = ax.elev-1.0\n",
    "        ax.azim = ax.azim+1.1 #pan down slower and rotate same speed\n",
    "    if n > 64 and n < 74:\n",
    "        ax.elev = ax.elev-0.5\n",
    "        ax.azim = ax.azim+1.1 #pan down slowly and rotate same speed\n",
    "    if n > 73 and n < 77:\n",
    "        ax.elev = ax.elev-0.2\n",
    "        ax.azim = ax.azim+0.5 #end by panning/rotating slowly to stopping position\n",
    "    \n",
    "    if n > 76: #add axis labels at the end, when the plot isn't moving around\n",
    "        ax.set_xlabel('Population (t)')\n",
    "        ax.set_ylabel('Population (t + 1)')\n",
    "        ax.set_zlabel('Population (t + 2)')\n",
    "    \n",
    "    # add a figure title to each plot then save the figure to the disk\n",
    "    fig.suptitle('Logistic Map, r=3.6 to r=4.0', fontsize=16, x=0.5, y=0.85)\n",
    "    plt.savefig('{}/{}/img{:03d}.png'.format(save_folder, gif_filename, n), bbox_inches='tight')\n",
    "\n",
    "# don't display the static plot\n",
    "plt.close()\n",
    "\n",
    "# load all the static images into a list then save as an animated gif\n",
    "gif_filepath = '{}/{}.gif'.format(save_folder, gif_filename)\n",
    "images = [Image.open(image) for image in glob.glob('{}/*.png'.format(working_folder))]\n",
    "gif = images[0]\n",
    "gif.info['duration'] = 10 #milliseconds per frame\n",
    "gif.info['loop'] = 0 #how many times to loop (0=infinite)\n",
    "gif.save(fp=gif_filepath, format='gif', save_all=True, append_images=images[1:])\n",
    "IPdisplay.Image(url=gif_filepath)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true,
    "deletable": true,
    "editable": true
   },
   "source": [
    "## Now zoom into the 3D plot"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {
    "collapsed": false,
    "deletable": true,
    "editable": true
   },
   "outputs": [],
   "source": [
    "# run the model for 4,000 generations for 50 growth rate parameters between 3.6 and 4.0\n",
    "pops = simulate(num_gens=4000, rate_min=3.6, rate_max=4.0, num_rates=50)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {
    "collapsed": false,
    "deletable": true,
    "editable": true,
    "scrolled": true
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<img src=\"images/phase-animate/05-logistic-3d-phase-diagram-chaotic-regime.gif\"/>"
      ],
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# set a filename and create the plot\n",
    "gif_filename = '05-logistic-3d-phase-diagram-chaotic-regime'\n",
    "working_folder = '{}/{}'.format(save_folder, gif_filename)\n",
    "if not os.path.exists(working_folder):\n",
    "    os.makedirs(working_folder)\n",
    "    \n",
    "fig, ax = phase_diagram_3d(pops, color='viridis', color_reverse=False, show=False, save=False)\n",
    "\n",
    "# configure the initial viewing perspective\n",
    "ax.elev = 25.\n",
    "ax.azim = 321.\n",
    "ax.dist = 11.0\n",
    "\n",
    "# zoom in to reveal the 3-D structure of the strange attractor\n",
    "for n in range(0, 100):\n",
    "    if n <= 18:\n",
    "        ax.azim = ax.azim-0.2 #begin by rotating very slowly\n",
    "    if n >= 19 and n <= 29:\n",
    "        ax.azim = ax.azim-10\n",
    "        ax.dist = ax.dist-0.05\n",
    "        ax.elev = ax.elev-2 #quickly whip around to the other side\n",
    "    if n >= 33 and n <= 49:\n",
    "        ax.azim = ax.azim+3\n",
    "        ax.dist = ax.dist-0.55\n",
    "        ax.elev = ax.elev+1.4 #zoom into the center\n",
    "    if n >= 61 and n <= 79:\n",
    "        ax.azim = ax.azim-2\n",
    "        ax.elev = ax.elev-2\n",
    "        ax.dist = ax.dist+0.2 #pull back and pan up\n",
    "    if n >= 80:\n",
    "        ax.azim = ax.azim-0.2 #end by rotating very slowly\n",
    "    \n",
    "    # add a figure title to each plot then save the figure to the disk\n",
    "    fig.suptitle('Logistic Map, r=3.6 to r=4.0', fontsize=16, x=0.5, y=0.85)\n",
    "    plt.savefig('{}/{}/img{:03d}.png'.format(save_folder, gif_filename, n), bbox_inches='tight')\n",
    "\n",
    "# don't display the static plot\n",
    "plt.close()\n",
    "\n",
    "# load all the static images into a list then save as an animated gif\n",
    "gif_filepath = '{}/{}.gif'.format(save_folder, gif_filename)\n",
    "images = [Image.open(image) for image in glob.glob('{}/*.png'.format(working_folder))]\n",
    "gif = images[0]\n",
    "gif.info['duration'] = 10 #milliseconds per frame\n",
    "gif.info['loop'] = 0 #how many times to loop (0=infinite)\n",
    "gif.save(fp=gif_filepath, format='gif', save_all=True, append_images=images[1:])\n",
    "IPdisplay.Image(url=gif_filepath)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "collapsed": true,
    "deletable": true,
    "editable": true
   },
   "source": [
    "## For more information about chaos theory, the logistic map, bifurcation plots, phase diagrams, and strange attractors, check out my write-up:\n",
    "http://geoffboeing.com/2015/03/chaos-theory-logistic-map/\n",
    "<br />and<br />\n",
    "http://geoffboeing.com/2015/04/visualizing-chaos-and-randomness/\n",
    "<br />and<br />\n",
    "http://geoffboeing.com/2015/04/animated-3d-plots-python/"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "collapsed": true,
    "deletable": true,
    "editable": true
   },
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "anaconda-cloud": {},
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 0
}
